package com.easyUpLoader.client;

import com.common.result.Result;
import com.common.util.StringUtil;
import com.easyUploader.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;

import static com.easyUploader.property.LocalFileProperty.DEFAULT_CHUNK_SIZE;

/**
 * 简易的客户端文件上传工具
 *
 * @author 大忽悠
 * @create 2022/10/30 15:33
 */
@Slf4j
public class SimpleClientUploader {
    private final RestTemplate restTemplate;
    private final String DOMAIN;
    private final String DEFAULT_BUCKET = "default";

    public SimpleClientUploader(RestTemplate restTemplate, String domain) {
        this.restTemplate = restTemplate;
        DOMAIN = domain;
    }

    /**
     * @param path    文件或者目录的路径
     * @param bucket  桶路径
     * @param dirPath 桶内文件存放的目录路径,如果没有则传入null
     * @return
     */
    public List<String> uploadFile(String path, String bucket, String dirPath) throws IOException {
        return uploadFile(path, pathMerge(bucket, dirPath));
    }

    /**
     * @param bucket  如果bucket为空则采用默认bucket
     * @param dirPath 如果dirPath为空,文件都上传到bucket下面,否则上传到bucket下的dirPath下去
     */
    private String pathMerge(String bucket, String dirPath) {
        return StringUtil.isNullOrEmpty(bucket) ? StringUtil.pathMerge(false,DEFAULT_BUCKET, dirPath) : StringUtil.pathMerge(false,bucket, dirPath);
    }

    /**
     * 根据文件大小采用不同的上传策略: <br>
     * 文件小于一个分片大小,则采用文件单分片上传,否则采用多件多分片上传
     *
     * @param path    可以传入文件或者目录路径
     * @param dirPath 目录路径,如果没有则传入null
     * @return
     */
    private List<String> uploadFile(String path, String dirPath) throws IOException {
        File file = new File(path);
        List<String> visitUrls=new ArrayList<>();
        uploadFile(file, file.isFile()?"":file.getName(), dirPath,visitUrls);
        return visitUrls;
    }

    @Nullable
    private void uploadFile(File currentFile, String parentDirPath, String dirPath,List<String> visitUrls) throws IOException {
        //如果是文件则直接上传
        if (!currentFile.isDirectory()) {
            log.info("当前上传文件在oss服务器上存放路径为: {}", StringUtil.pathMerge(false,dirPath, parentDirPath, currentFile.getName()));
            String visitUlr = doUploadFile(currentFile, StringUtil.pathMerge(false,dirPath, parentDirPath));
            visitUrls.add(visitUlr);
            return;
        }
        //遍历目录,递归处理
        for (File f : currentFile.listFiles()) {
            //dirPath加上当前目录相对于path的路径
           uploadFile(f, f.isDirectory() ? StringUtil.pathMerge(false,parentDirPath, f.getName()) : parentDirPath, dirPath,visitUrls);
        }
    }

    private String doUploadFile(File testUpLoadFile, String dirPath) throws IOException {
        MultiValueMap<String, Object> commonParams = buildCommonParams(testUpLoadFile, dirPath);
        HttpHeaders headers = buildHeaders();
        //单分片任务
        Integer totalChunk = (Integer) commonParams.get("totalChunk").get(0);
        if (totalChunk.equals(1)) {
            singleSliceHandle(testUpLoadFile, commonParams);
        } else {
            //多分片上传
            return multiplySliceHandle(testUpLoadFile, commonParams, totalChunk, headers);
        }
        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(commonParams, headers);
        return (String) restTemplate.postForObject(DOMAIN + "/uploader/chunkUpload", requestEntity, Result.class).getData();
    }


    private String multiplySliceHandle(File file, MultiValueMap<String, Object> commonParams, Integer totalChunk, HttpHeaders headers) throws IOException {
        String result = null;
        //当前大文件是否可以进行秒传,如果不可以秒传,那么返回已经上传的分片集合
        List<Integer> uploadedTrunkList = new ArrayList<>();
        if ((result = speedUploadCheck(commonParams, headers, uploadedTrunkList)) != null) {
            log.info("当前文件采用秒传策略");
            return result;
        }
        //一个分片多大
        Long chunkSize = (Long) commonParams.get("chunkSize").get(0);
        //构建随机读写文件流
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        //依次将每个分片发送到服务器---顺序发送
        //分片序号从1开始
        for (int i = 1; i <= totalChunk; i++) {
            //当前分片已经上传过了,直接跳过
            if (uploadedTrunkList.contains(i)) {
                continue;
            }
            clearCustomParams(commonParams);
            //1.获取到当前分片对应的那一块字节流,如果当前分片是最后一块分片,那么就将剩余数据全部封装在当前分片内发出
            commonParams.add("chunkNumber", i);
            Long currentChunkIndex = chunkSize * (i - 1);
            randomAccessFile.seek(currentChunkIndex);
            if (i == totalChunk) {
                doHandleSlice(file, commonParams, randomAccessFile, file.length() - currentChunkIndex);
            } else {
                //2.封装为ByteArrayResource
                doHandleSlice(file, commonParams, randomAccessFile, chunkSize);
            }
            //3.发送当前分片数据
            HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(commonParams, headers);
            //获取第一个分片上传成功后返回的重命名后的文件名
            result = (String) restTemplate.postForObject(DOMAIN + "/uploader/chunkUpload", requestEntity, Result.class).getData();
        }
        return result;
    }

    private String speedUploadCheck(MultiValueMap<String, Object> commonParams, HttpHeaders headers, List<Integer> uploadedTrunkList) {
        LinkedHashMap dataMap = (LinkedHashMap) restTemplate.getForObject(DOMAIN + "/uploader/checkFileUpload?fileMd5=" + commonParams.get("fileIdentifier").get(0), Result.class).getData();
        //当前文件已经上传了
        if (((Boolean) dataMap.get("uploaded"))) {
            return (String) dataMap.get("visitUrl");
        }
        uploadedTrunkList.addAll((Collection<? extends Integer>) dataMap.get("readChunkList"));
        return null;
    }

    private void clearCustomParams(MultiValueMap<String, Object> commonParams) {
        commonParams.remove("dataSize");
        commonParams.remove("chunkIdentifier");
        commonParams.remove("file");
        commonParams.remove("chunkNumber");
    }

    private void doHandleSlice(File file,
                               MultiValueMap<String, Object> commonParams,
                               RandomAccessFile randomAccessFile,
                               Long sendDataSize) throws IOException {
        byte[] bytes = new byte[Math.toIntExact(sendDataSize)];
        randomAccessFile.read(bytes, 0, Math.toIntExact(sendDataSize));
        commonParams.add("dataSize", sendDataSize);
        commonParams.add("chunkIdentifier", FileUtil.getMd5(bytes));
        commonParams.add("file", new ByteArrayResource(bytes) {
            @Override
            public long contentLength() {
                return sendDataSize;
            }

            @Override
            public String getFilename() {
                return file.getName();
            }
        });
    }

    private void singleSliceHandle(File file, MultiValueMap<String, Object> commonParams) {
        commonParams.add("file", new FileSystemResource(file));
        commonParams.add("chunkIdentifier", commonParams.get("fileIdentifier").get(0));
        commonParams.add("chunkNumber", 1);
        commonParams.add("dataSize", commonParams.get("totalSize").get(0));
    }

    private HttpHeaders buildHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        return headers;
    }

    /**
     * 构建通用的请求参数
     */
    private MultiValueMap<String, Object> buildCommonParams(File file, String dirPath) {
        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
        params.add("fileIdentifier", FileUtil.getMd5(file));
        params.add("chunkSize", DEFAULT_CHUNK_SIZE);
        params.add("totalSize", file.length());
        params.add("totalChunk", file.length() < DEFAULT_CHUNK_SIZE ? 1 : (int) (file.length() / DEFAULT_CHUNK_SIZE));
        params.add("fileName", file.getName());
        //如果目录路径不为空,那么就设置为通用属性
        if (!StringUtil.isNullOrEmpty(dirPath)) {
            params.add("directoryPath", dirPath);
        }
        return params;
    }
}
